package com.tn.rtdata;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.tn.openrpc.RemoteMethod;
import com.tn.openrpc.RemoteMethodException;
import com.tn.openrpc.TimeoutException;

/**
 * <code>Session</code> is a <i>singleton</i> that represents a client's connection to the live-data services.
 */
public class Session
{
  private static final Logger LOGGER = LoggerFactory.getLogger(Session.class);

  private static int threadCount;

  private boolean closed;
  private RemoteMethod closeMethod;
  private long heartbeatInterval;
  private RemoteMethod heartbeatMethod;
  private boolean heartbeatRunning;
  private String sessionId;
  private List<Listener> listeners;

  /**
   * Creates a new <code>Session</code>.
   *
   * @param connectMethod         the method used to connect to the live-data services.
   * @param closeMethod           the method used to close the session.
   * @param heartbeatMethod       the method used to heartbeat.
   * @param heartbeatInterval     the interval of the heartbeats.
   *
   * @throws SessionException if an error occurs setting up the session.
   */
  public Session(
    RemoteMethod connectMethod,
    RemoteMethod closeMethod,
    RemoteMethod heartbeatMethod,
    long heartbeatInterval
  )
    throws SessionException
  {
    this(connectMethod, closeMethod, heartbeatMethod, heartbeatInterval, new ArrayList<Listener>());
  }

  /**
   * Creates a new <code>Session</code>.
   *
   * @param connectMethod         the method used to connect to the live-data services.
   * @param closeMethod           the method used to close the session.
   * @param heartbeatMethod       the method used to heartbeat.
   * @param heartbeatInterval     the interval of the heartbeats.
   * @param listeners             the listeners.
   *
   * @throws SessionException if an error occurs setting up the session.
   */
  public Session(
    RemoteMethod connectMethod,
    RemoteMethod closeMethod,
    RemoteMethod heartbeatMethod,
    long heartbeatInterval,
    List<Listener> listeners
  )
    throws SessionException
  {
    try
    {
      this.sessionId = (String)connectMethod.invoke();
      this.closeMethod = closeMethod;
      this.heartbeatMethod = heartbeatMethod;
      this.heartbeatInterval = heartbeatInterval;
      this.listeners = new ArrayList<Listener>(listeners);
      this.closed = false;

      startHeartbeat();

      LOGGER.info("Created session with id: {}", this.sessionId);
    }
    catch (RemoteMethodException e)
    {
      throw new SessionException(e);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void finalize() throws Throwable
  {
    close();
    super.finalize();
  }

  /**
   * Returns the session id.
   */
  public String getId()
  {
    return sessionId;
  }

  /**
   * Returns the closed flag.
   */
  public boolean isClosed()
  {
    return closed;
  }

  /**
   * Adds a <code>listener</code>.
   */
  public void addListener(Listener listener)
  {
    listeners.add(listener);
  }

  /**
   * Adds a <code>listener</code>.
   */
  public void removeListener(Listener listener)
  {
    listeners.remove(listener);
  }

  /**
   * Closes this session.
   */
  public void close() throws SessionException
  {
    try
    {
      if (!isClosed())
      {
        LOGGER.info("Closing session with id: {}", sessionId);
        heartbeatRunning = false;
        closeMethod.invoke(sessionId);
        closed = true;
      }
    }
    catch (RemoteMethodException e)
    {
      throw new SessionException(e);
    }
  }

  /**
   * Fires the <code>Listener.heartbeatSucceeded()</code> method.
   */
  private void fireListenersHeartbeatSucceeded()
  {
    for (Listener sessionListener : new ArrayList<Listener>(listeners))
    {
      sessionListener.heartbeatSucceeded();
    }
  }

  /**
   * Fires the <code>Listener.heartbeatFailed()</code> method.
   */
  private void fireSessionListenersHeartbeatFailed()
  {
    for (Listener sessionListener : new ArrayList<Listener>(listeners))
    {
      sessionListener.heartbeatFailed();
    }
  }

  /**
   * Starts the heartbeat thread.
   */
  private void startHeartbeat()
  {
    heartbeatRunning = true;

    Thread heartbeatThread = new Thread(new HeartbeatMonitor(), "heartbeat-" + threadCount++);
    heartbeatThread.setDaemon(true);
    heartbeatThread.start();
  }

  /**
   * The listener interface used to pass events relating to the session heartbeats.
   */
  public static abstract class Listener
  {
    /**
     * Invoked when a heartbeat was successfully sent.
     */
    public void heartbeatSucceeded()
    {
      //Do nothing, specializations of this class can override if they need to.
    }

    /**
     * Invoked when a heartbeat was failed.
     */
    public void heartbeatFailed()
    {
      //Do nothing, specializations of this class can override if they need to.
    }
  }

  /**
   * An implementation of <code>Runnable</code> that invokes the <code>heartbeatMethod</code>.
   */
  private class HeartbeatMonitor implements Runnable
  {
    /**
     * {@inheritDoc}
     */
    @Override
    public void run()
    {
      while (heartbeatRunning)
      {
        try
        {
          LOGGER.debug("Heartbeat sent for session with id: {}", sessionId);
          heartbeatMethod.invoke(sessionId);
          fireListenersHeartbeatSucceeded();
          Thread.sleep(heartbeatInterval);
        }
        catch (InterruptedException e)
        {
          LOGGER.debug("Heartbeat thread interrupted.", e);
        }
        catch (TimeoutException e)
        {
          LOGGER.error("Heartbeat failed.", e);
          fireSessionListenersHeartbeatFailed();
        }
        catch (RemoteMethodException e)
        {
          LOGGER.error("Heartbeat failed.", e);
        }
      }
    }
  }
}
